# -*- coding:utf-8 -*-
"""插件类"""
import array
import json
import os
import platform
import sys
import math
from selenium import webdriver
from time import sleep
import getpass
base_dir = os.path.dirname(os.path.abspath(__file__))
# lib_dir = rfc.get_config("$.virtual_settings.lib_dir").current
# lib_dir = "/Users/metoto/VirtualEnv/apiAutoTest-v3/lib/python3.8/site-packages"
sys.path.append(base_dir)
# sys.path.append(lib_dir)

import re
from copy import deepcopy
from string import Template
from zipfile import ZipFile
from zipfile import ZIP_DEFLATED
from datetime import datetime
from typing import Optional
from typing import Dict
from typing import Any
from typing import Union
from decimal import Decimal

import allure
import paramiko
import pymysql
import requests
import xlrd
import yagmail
# import yaml
import ruamel
from ruamel import yaml
from ruamel.yaml import scalarstring
from jsonpath import jsonpath
from loguru import logger
from requests import Session
from requests import Response
from _pytest.outcomes import Skipped
import pytest
import urllib3
from hooks import *


class ReadFileClass:
    """文件读取类"""

    def __init__(self, path: str, case_expr: str = "$.file_path.test_case"):
        self.path = path
        self.current: Optional[Union[Dict[str, Any], str]] = None
        self._config: Optional[Dict[str, Any]] = None
        self._testcase: Optional[Dict[str, Any]] = None
        self.case_expr = case_expr

    @property
    def config(self):
        if self._config is None:
            self.read()
        return self._config

    @config.setter
    def config(self, value):
        self._config = value

    def read(self):
        with open(self.path, "r", encoding="utf-8") as file:
            # self.config = yaml.safe_load(file.read())
            yaml = ruamel.yaml.YAML(typ='safe', pure=True)
            self.config = yaml.load(file.read())

    def get_config(self, expr: str):
        """获取配置项，传入jsonpath表达式"""
        try:
            self.current = jsonpath(self.config, expr)[0]
        except IndexError:
            self.current = jsonpath(self.config, expr)
        return self

    def get_case(self):
        self.get_config(self.case_expr)
        book = xlrd.open_workbook(self.current)
        # 读取第一个sheet页
        table = book.sheet_by_index(0)
        for norw in range(1, table.nrows):
            yield table.row_values(norw)

    @property
    def testcase(self):
        if self._testcase is None:
            self.read_case()
        return self._testcase

    @testcase.setter
    def testcase(self, value):
        self._testcase = value

    def read_case(self):
        with open(self.path, "r", encoding="utf-8") as file:
            # self.testcase = yaml.safe_load(file.read())
            yaml = ruamel.yaml.YAML(typ='safe', pure=True)
            self.testcase = yaml.load(file.read())

    def get_case_by_yaml(self, expr: str):
        try:
            self.current = jsonpath(self.testcase, expr)[0]
        except IndexError:
            self.current = jsonpath(self.testcase, expr)
        return self

    @classmethod
    def create_test_yaml_file(cls):
        '''
        将一个dict写成yaml文件
        使用x作为标记在yaml文件中添加引号字符串的字段
        '''
        x = ruamel.yaml.scalarstring.DoubleQuotedScalarString
        D = {'name': x('mydata'), 'value': {'x': 1, 'y': 2, 'z': 3}}
        test_case_content_0 = {
            "test_case": [{"caseid": x("c1"), "case_desc": "login1"}, {"caseid": "c2", "case_desc": "logout1"}]}
        test_case_content_1 = {
            "test_case": [{"caseid": 'c3', "case_desc": "login2"}, {"caseid": "c4", "case_desc": "logout2"}]}
        test_case_content_2 = {"test_case": [{'caseid': "c5", 'case_desc': 'login3'}]}
        yaml = ruamel.yaml.YAML()
        yaml.indent(mapping=4)
        with open(r'testcase.yaml', 'w') as f:
            yaml.dump([test_case_content_0, test_case_content_1, test_case_content_2], f)
        # 1 get the yaml location, e.g. testcase/testproject/view/casename_202204081654.yaml
        # 2 get the yaml content

        # with open(r'testcase.yaml', 'w') as f:
        #     yaml.dump([test_case_content_0,test_case_content_1,test_case_content_2],f)
        pass

    @classmethod
    def get_test_yaml_file(cls):
        file_location = "testcase.yaml"
        with open(file_location) as f:
            y = yaml.safe_load(f)
            print(y)
        with open(r'testcase.yaml', 'w') as f:
            yaml.dump(y, f)

    @classmethod
    def get_path(cls, pathlist: list):
        path = ""
        for node in pathlist:
            path += node + "/"
        return path[:len(path) - 1]
        pass

    @classmethod
    def import_case_from_json_file(cls):
        with open('postman/Stream.postman_collection.json', 'r') as f:
            collection_json_str = f.read()
        cases_dict = json.loads(collection_json_str)
        cases = cases_dict["item"]
        print(cases)
        print(len(cases))
        for case in cases:
            print(case['name'])
            print(case['request']['method'])
            # print(case['request']['url']['protocol'])
            print(cls.get_path(case['request']['url']['path']))
            if case['request']['method'] in ["get", "GET"]:
                query_list = case['request']['url']['query']
                for query in query_list:
                    print(query["key"] + ": " + query['value'])
                print(case['request']['url']['query'])
                pass
            elif case['request']['method'] in ["post", "POST"]:
                query_list = case['request']['url']['query']
                print(query_list)
                for query in query_list:
                    print(query["key"] + ": " + query['value'])
                print(case['request']['url']['query'])
                pass
            print("===================================================")

        pass

    # 创建指定地址的py文件
    @classmethod
    def create_test_py_file(cls, yaml_case_file_path, py_file=""):
        with open(cls('config.yaml').get_config('$.file_path.case_tmp').current, encoding='utf-8') as f:
            file_str = f.read()
        file_content = file_str % (yaml_case_file_path)
        open(py_file, 'w', encoding='utf-8').write(file_content)
        pass

    # 传入文件夹时，给文件夹下所有的yamlcase生成py文件
    @classmethod
    def create_files_from_path(cls, folder_path):
        files = os.listdir(folder_path)
        for f in files:
            sub_f_path = os.path.join(folder_path, f)
            if os.path.isdir(sub_f_path):
                cls.create_files_from_path(sub_f_path)
            else:
                if os.path.splitext(sub_f_path)[-1] == '.yaml':
                    # 生成py文件
                    py_file_name = os.path.basename(sub_f_path).split('.')[0] + ".py"
                    py_file_name = os.path.join(folder_path, py_file_name)
                    cls.create_test_py_file(sub_f_path, py_file_name)

    @classmethod
    def remove_py_files(cls, case_folder):
        # case_folder = rfc.get_config("$.file_path.case_folder").current
        files = os.listdir(case_folder)
        for f in files:
            sub_f_path = os.path.join(case_folder, f)
            if os.path.isdir(sub_f_path):
                cls.remove_py_files(sub_f_path)
            else:
                if os.path.splitext(sub_f_path)[-1] == '.py':
                    # 删除py文件
                    # print(f"delete py file as name {os.path.basename(sub_f_path).split('.')[0]}.py")
                    os.remove(sub_f_path)

    @classmethod
    def generate_cases(cls, case_path, cmd_arg=""):
        if isinstance(case_path, list):
            for pj in case_path:
                # --TO-DO--
                # print(f"sub pj is {pj}")

                # 将目录下的所有yaml文件取出来，并生成同名pyfile,保存在yaml文件的目录
                case_folder = rfc.get_config("$.file_path.case_folder").current
                project_folder = os.path.join(base_dir, case_folder, pj)
                ReadFileClass.get_files_from_path(project_folder)
                # ++tbd+++
                pass
        else:
            case_folder = rfc.get_config("$.file_path.case_folder").current
            if os.path.isdir(os.path.join(base_dir, case_folder, case_path)) and cmd_arg in ["c"]:
                raise Exception("case路径传参有误")
            elif os.path.isfile(os.path.join(base_dir, case_folder, case_path)) and cmd_arg in ["p", "m", ""]:
                raise Exception("project/module路径传参有误")
            elif os.path.isdir(os.path.join(base_dir, case_folder, case_path)) and cmd_arg in ["p", "m", ""]:
                project_folder = os.path.join(base_dir, case_folder, case_path, "")
                ReadFileClass.create_files_from_path(project_folder)
            elif os.path.isfile(os.path.join(base_dir, case_folder, case_path)) and cmd_arg in ["c"]:
                if os.path.splitext(case_path)[-1] == '.yaml':
                    yaml_file_path = os.path.join(base_dir, case_folder, case_path)
                    py_file_path = os.path.join(base_dir, case_folder, case_path).split('.')[0] + ".py"
                    ReadFileClass.create_test_py_file(yaml_file_path, py_file_path)
                else:
                    raise AssertionError(f"当前指定的用例文件不是yaml文件或者不存在！")



rfc = ReadFileClass("config.yaml")


class DataBaseMysql:
    """mysql 操作类"""

    def __init__(self, config: ReadFileClass):
        mysql_conf = config.get_config("$.database").current
        self._result = None
        if "ssh_server" in mysql_conf.keys():
            del mysql_conf["ssh_server"]
        self.con = pymysql.connect(
            **mysql_conf, cursorclass=pymysql.cursors.DictCursor, charset="utf8mb4"
        )

    @property
    def result(self):
        return self._result

    @result.setter
    def result(self, value):
        try:
            json.dumps(value)
        except TypeError:
            for k, v in value.items():
                if isinstance(
                        v,
                        (
                                datetime,
                                Decimal,
                        ),
                ):
                    value[k] = str(v)
        self._result = value

    def __enter__(self):
        logger.success("数据库连接成功")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        logger.success("数据库关闭成功")
        self.con.close()

    def execute_sql(self, sql_str: str):
        with self.con.cursor() as csr:
            csr.execute(sql_str)
            self.result = csr.fetchone()
            self.con.commit()
        logger.debug(f"执行SQL: {sql_str}, {self.result}")


class EmailServe:
    """邮件服务类"""

    def __init__(self, config: ReadFileClass):
        self.email_conf = config.get_config("$.email").current
        self.zip_conf = config.get_config("$.file_path.report").current
        self.zip_name = "report.zip"

    def email(self):
        """邮件服务"""
        with yagmail.SMTP(**self.email_conf["serve"]) as yag:
            yag.send(**self.email_conf["context"])

    def zip(self):
        """压缩报告"""
        with ZipFile(self.zip_name, "w", ZIP_DEFLATED) as zp:
            for path, _, filenames in os.walk(self.zip_conf):
                # 去掉目标跟路径，只对目标文件夹下边的文件及文件夹进行压缩
                fpath = path.replace(self.zip_conf, "")

                for filename in filenames:
                    zp.write(
                        os.path.join(path, filename), os.path.join(fpath, filename)
                    )

    def serve(self):
        logger.info("报告压缩中...")
        self.zip()
        self.email()
        os.remove(self.zip_name)
        logger.success("邮件已发送...")


class RemoteServe:
    """远程服务器"""

    def __init__(
            self,
            host: str,
            port: int = 22,
            username: str = "root",
            password: str = None,
            private_key_file: str = None,
            private_password: str = None,
    ):
        # 进行SSH连接
        self.trans = paramiko.Transport((host, port))
        self.host = host
        if password is None:
            self.trans.connect(
                username=username,
                pkey=paramiko.RSAKey.from_private_key_file(
                    private_key_file, private_password
                ),
            )
        else:
            self.trans.connect(username=username, password=password)
        # 将sshclient的对象的transport指定为以上的trans
        self.ssh = paramiko.SSHClient()
        logger.success("SSH客户端创建成功.")
        self.ssh._transport = self.trans
        # 创建SFTP客户端
        self.ftp_client = paramiko.SFTPClient.from_transport(self.trans)
        logger.success("SFTP客户端创建成功.")

    def execute_cmd(self, cmd: str):
        """
        :param cmd: 服务器下对应的命令
        """
        stdin, stdout, stderr = self.ssh.exec_command(cmd)
        error = stderr.read().decode()
        logger.info(f"输入命令: {cmd} -> 输出结果: {stdout.read().decode()}")
        logger.warning(f"异常信息: {error}")
        return error

    def files_action(
            self, post: bool, local_path: str = os.getcwd(), remote_path: str = "/root"
    ):
        """
        :param post: 动作 为 True 就是上传， False就是下载
        :param local_path: 本地的文件路径， 默认当前脚本所在的工作目录
        :param remote_path: 服务器上的文件路径，默认在/root目录下
        """
        if post:  # 上传文件
            self.execute_cmd("mkdir backup_sql")
            self.ftp_client.put(
                localpath=local_path,
                remotepath=f"{remote_path}{os.path.split(local_path)[1]}",
            )
            logger.info(
                f"文件上传成功: {local_path} -> {self.host}:{remote_path}{os.path.split(local_path)[1]}"
            )
        else:  # 下载文件
            if not os.path.exists(local_path):
                os.mkdir(local_path)
            file_path = local_path + os.path.split(remote_path)[1]
            self.ftp_client.get(remotepath=remote_path, localpath=file_path)
            logger.info(f"文件下载成功: {self.host}:{remote_path} -> {file_path}")

    def ssh_close(self):
        """关闭连接"""
        self.trans.close()
        logger.info("已关闭SSH连接...")


class DataClear:
    """数据隔离实现"""

    def __init__(self, config: ReadFileClass):
        self.cfg = config.get_config("$.database").current
        self.server = None
        # 导出的sql文件名称及后缀
        self.file_name = (
            f"{self.cfg.get('db')}_{datetime.now().strftime('%Y-%m-%dT%H_%M_%S')}.sql"
        )

        self.c_name = self.cfg.get("ssh_server").get("mysql_container")
        self.mysql_user = self.cfg.get("user")
        self.mysql_passwd = self.cfg.get("password")
        self.mysql_db = self.cfg.get("db")

        self.local_backup = self.cfg.get("ssh_server").get("sql_data_file")
        self.remote_backup = "/root/backup_sql/"

        # mysql 备份命令
        self.backup_cmd = f"mysqldump -h127.0.0.1 -u{self.mysql_user} -p{self.mysql_passwd} {self.mysql_db}"
        # mysql 还原
        self.recovery_cmd = f"mysql -h127.0.0.1 -u{self.mysql_user} -p{self.mysql_passwd} {self.mysql_db}"

    def backup(self):
        """备份操作"""
        if self.c_name is None:
            cmd = f"{self.backup_cmd}  > {self.file_name}"
        else:
            cmd = f"docker exec -i {self.c_name} {self.backup_cmd} > {self.remote_backup}{self.file_name}"
        self.server.execute_cmd(cmd)

        self.server.files_action(
            0, f"{self.local_backup}", f"{self.remote_backup}{self.file_name}"
        )
        logger.info("备份完成...")

    def recovery(self):
        """还原操作"""
        result = self.server.execute_cmd(f"ls -l {self.remote_backup}{self.file_name}")
        if "No such file or directory" in result:
            # 本地上传
            self.server.files_action(
                1, f"{self.local_backup}{self.file_name}", self.remote_backup
            )
        cmd = f"docker exec -i {self.c_name} {self.recovery_cmd} < {self.remote_backup}{self.file_name}"
        self.server.execute_cmd(cmd)
        logger.success("成功还原...")

    def __enter__(self):
        # 深拷贝
        ssh_cfg = deepcopy(self.cfg.get("ssh_server"))
        del ssh_cfg["mysql_container"]
        del ssh_cfg["sql_data_file"]
        self.server = RemoteServe(host=self.cfg.get("host"), **ssh_cfg)
        # 新建backup_sql文件夹在服务器上，存放导出的sql文件
        self.server.execute_cmd("mkdir backup_sql")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.server.ssh_close()


class ReportStyle:
    """allure 报告样式"""

    @staticmethod
    def step(step: str, var: Optional[Union[str, Dict[str, Any]]] = None):
        with allure.step(step):
            allure.attach(
                json.dumps(var, ensure_ascii=False, indent=4),
                "附件内容",
                allure.attachment_type.JSON,
            )

    @staticmethod
    def title(title: str):
        allure.dynamic.title(title)


class DataProcess:
    """数据依赖实现"""

    extra_pool = {}
    assert_type_list = ["eq",
                        "not_eq",
                        "gt",
                        "lt",
                        "btw",
                        "istype",
                        "intype",
                        "in",
                        "contain",
                        "len",
                        "size", ]

    def __init__(self, config: ReadFileClass):
        self.config = config
        self._headers = None
        self._path = None
        self._body = None
        self._sql = None
        self._files = None
        self._skip = None

    @property
    def skip(self):
        return self._skip

    @skip.setter
    def skip(self, value):
        if isinstance(value, int):
            if value:
                raise Skipped("跳过用例")
        elif eval(self.rep_expr(value).capitalize()):
            raise Skipped("跳过用例")

    @property
    def headers(self):
        return self._headers

    @headers.setter
    def headers(self, value):
        self._headers = self.config.get_config("$.request_headers").current
        if value != "" and value is not None:
            self._headers.update(DataProcess.handle_data(value))
        else:
            return None

    @property
    def path(self):
        return self._path

    @path.setter
    def path(self, value):
        self.config.get_config("$.server.current_server")
        # print("========处理路径参数化===========")
        self._path = f"{self.config.current}{DataProcess.rep_expr(value)}"

    @property
    def body(self):
        return self._body

    @body.setter
    def body(self, value):
        if self._body != "":
            # print("===========处理body参数化================")
            self._body = DataProcess.handle_data(value)

    @property
    def files(self):
        return self._files

    @files.setter
    def files(self, value):
        if value != "" and value is not None:
            for k, v in DataProcess.handle_data(value).items():
                # 多文件上传
                if isinstance(v, list):
                    self._files = [(k, (open(path, "rb"))) for path in v]
                else:
                    # 单文件上传
                    self._files = {k: open(v, "rb")}
        else:
            self._files = None

    @property
    def sql(self):
        return self._sql

    @sql.setter
    def sql(self, value):
        if value != "" and value is not None:
            print("==========处理sql参数化============")
            self._sql = DataProcess.rep_expr(value)
        return None

    @classmethod
    def handle_data(cls, value: str) -> Optional[Dict[str, Any]]:
        """处理数据的方法"""
        if value == "":
            print("no value input")
            return
        if type(value) == dict:
            value = str(value)
        try:
            json_str = DataProcess.rep_expr(value)
            return json.loads(json_str)
        except json.decoder.JSONDecodeError:
            return eval(json_str)

    @classmethod
    def rep_expr(cls, content: str):
        # 首先使用模板将字符串中的$作为占位符识别，将${xxx}或者$xxx并返回替换成xxx赋值的字符串
        # content = '[{\"id\":\"$uuid\",\"parentId\":\"0\",\"label\":\"A\",\"value\":\"$uuid\",\"children\":""}]'
        # s1 = Template(content).safe_substitute({"uuid":"123"})
        # 得到的s1就是'[{\"id\":\"123\",\"parentId\":\"0\",\"label\":\"A\",\"value\":\"123\",\"children\":""}]'
        content = Template(content).safe_substitute(DataProcess.extra_pool)
        for func in re.findall("\\${(.*?)}", content):
            try:
                # 将占位符${xxx}中的xxxx作为函数名，尝试去执行，然后将执行的结果代替${xxx}
                # 如果没有找到，则会捕获到exception
                content = content.replace("${%s}" % func, DataProcess.exec_func(func))
            except Exception as e:
                logger.error(e)
        return content

    def handle_case(self, path, header, skip_expr, send_content, file):
        self.path = path
        self.headers = header
        self.skip = skip_expr
        self.body = send_content
        self.files = file

    def handle_sql(self, db_session: DataBaseMysql):
        for sql_str in self.sql.split(";"):
            sql_str = sql_str.strip()
            if sql_str == "":
                continue
            # 查后置sql
            db_session.execute_sql(sql_str)
            ReportStyle.step(f"执行sql: {sql_str}", db_session.result)
            logger.info(f"执行sql: {sql_str} \n 结果: {db_session.result}")
            if db_session.result is not None:
                # 将查询结果添加到响应字典里面，作用在，接口响应的内容某个字段 直接和数据库某个字段比对，在预期结果中
                # 使用同样的语法提取即可
                DataProcess.extra_pool.update(db_session.result)

    @staticmethod
    def extractor(obj: dict, expr: str = ".") -> Any:
        """
        根据表达式提取字典中的value，表达式, . 提取字典所有内容， $.case 提取一级字典case， $.case.data 提取case字典下的data
        :param obj :json/dict类型数据
        :param expr: 表达式, . 提取字典所有内容， $.case 提取一级字典case， $.case.data 提取case字典下的data
        $.0.1 提取字典中的第一个列表中的第二个的值
        """
        try:
            result = jsonpath(obj, expr)[0]
        except Exception as e:
            logger.error(f"{expr} - 提取不到内容，丢给你一个错误！{e}")
            # result = expr
            result = False
        return result

    @classmethod
    def handle_extra(cls, extra: dict, response: dict, header: dict = None, param: dict = None):
        """
        处理提取参数栏
        :param extra_str: excel中 提取参数栏内容，需要是 {"参数名": "jsonpath提取式"} 可以有多个
        :param response: 当前用例的响应结果字典
        """
        if extra != {} and extra is not None:
            # extra_dict = json.loads(extra)
            for k, v in extra.items():
                print(k,v)
                if DataProcess.extractor(response, v):
                    DataProcess.extra_pool[k] = DataProcess.extractor(response, v)
                    logger.info(f"加入依赖字典,key: {k}, 对应value: {v}")
                elif DataProcess.extractor(header,v):
                    DataProcess.extra_pool[k] = DataProcess.extractor(header, v)
                    logger.info(f'【header】加入依赖字典,key: {k}, 对应value: {v}')
                elif cls.extractor(param,v):
                    DataProcess.extra_pool[k] = DataProcess.extractor(param, v)
                    logger.info(f'【send_content】加入依赖字典,key: {k}, 对应value: {v}')
                else:
                    DataProcess.extra_pool[k]  = cls.rep_expr(v)
                    logger.info(f'当前extra参数不在respone,header和request body里，尝试查找函数加入依赖字典,key: {k}, 对应value: {cls.rep_expr(v)}')
                    pass

    @classmethod
    def handle_extra_backup_01(cls, extra: dict, response: dict):
        """
        处理提取参数栏
        :param extra_str: excel中 提取参数栏内容，需要是 {"参数名": "jsonpath提取式"} 可以有多个
        :param response: 当前用例的响应结果字典
        """
        if extra != {}:
            # extra_dict = json.loads(extra)
            for k, v in extra.items():
                DataProcess.extra_pool[k] = DataProcess.extractor(response, v)
                logger.info(f"加入依赖字典,key: {k}, 对应value: {v}")

    @classmethod
    def handle_extra_backup(cls, extra_str: str, response: dict):
        """
        处理提取参数栏
        :param extra_str: excel中 提取参数栏内容，需要是 {"参数名": "jsonpath提取式"} 可以有多个
        :param response: 当前用例的响应结果字典
        """
        if extra_str != "":
            extra_dict = json.loads(extra_str)
            for k, v in extra_dict.items():
                DataProcess.extra_pool[k] = DataProcess.extractor(response, v)
                logger.info(f"加入依赖字典,key: {k}, 对应value: {v}")

    @classmethod
    def assert_result(cls, response: dict, assertion: dict, config: ReadFileClass, assert_mode=None):
        """预期结果实际结果断言方法
        :param response: 实际响应结果
        :param expect_str: 预期响应内容，从excel中读取
        return None
        """
        if assert_mode is None:
            assert_mode = config.get_config("$.run_settings.assert_mode").current
        # 后置sql变量转换
        ReportStyle.step("当前可用参数池", DataProcess.extra_pool)
        index = 0
        # for k, v in DataProcess.handle_data(expect_str).items():
        for k, v in DataProcess.handle_data(assertion).items():
            # 获取需要断言的实际结果部分
            actual = DataProcess.extractor(response, k)
            index += 1
            expect_value = v['value']
            assert_type = v['assert_type']
            if assert_type in cls.assert_type_list:
                if assert_type == "eq":
                    assert_type_desc = "值等于"
                    assert_res = Validator().isequal_validator(actual, expect_value)
                elif assert_type == "!eq":
                    assert_type_desc = "值不等于"
                    assert_res = Validator().isnot_equal_validator(actual, expect_value)
                elif assert_type == "gt":
                    assert_type_desc = "值大于等于"
                    assert_res = Validator().greater_validator(actual, expect_value)
                elif assert_type == "lt":
                    assert_type_desc = "值小于等于"
                    assert_res = Validator().less_than_validator(actual, expect_value)
                elif assert_type == "btw":
                    assert_type_desc = "值介于指定范围"
                    assert_res = Validator().between_validator(actual, expect_value)
                elif assert_type == "istype":
                    assert_type_desc = "值类型相等"
                    assert_res = Validator().istype_validator(actual, expect_value)
                elif assert_type == "intype":
                    assert_type_desc = "值类型匹配"
                    assert_res = Validator().intype_validator(actual, expect_value)
                elif assert_type == "len":
                    assert_type_desc = "值长度相等"
                    assert_res = Validator().len_validator(actual, expect_value)
                elif assert_type == "size":
                    assert_type_desc = "值列表size"
                    assert_res = Validator().size_validator(actual, expect_value)
                elif assert_type == "in":
                    assert_type_desc = "存在于"
                    assert_res = Validator().in_validator(actual, expect_value)
                elif assert_type == "contain":
                    assert_type_desc = "包含"
                    assert_res = Validator().contain_validator(actual, expect_value)
            else:
                raise AssertionError(f"当前断言类型有误,配置不存在{assert_type}")
            assert_info = {"提取实际结果路径": k, "实际结果": actual, "校验类型": assert_type_desc, "预期结果": expect_value,
                           "测试结果": assert_res}
            logger.info(f"断言{index}: {assert_info}")
            ReportStyle.step(f"断言{index}", assert_info)
            if assert_mode == "ignore_error":
                pytest.assume(assert_res,
                              f"第{index}个断言失败 -｜-实际结果：{actual} || 校验类型:{assert_type_desc} || 预期结果：{expect_value}")
            else:
                try:
                    assert assert_res
                except AssertionError:
                    raise AssertionError(
                        f'第{index}个断言失败 -|- 实际结果:{actual} || 校验类型:{assert_type_desc}|| 预期结果: {expect_value}')

    @staticmethod
    def exec_func(func: str) -> str:
        """执行函数(exec可以执行Python代码)
        :params func 字符的形式调用函数
        : return 返回的将是个str类型的结果
        """
        # 得到一个局部的变量字典，来修正exec函数中的变量，在其他函数内部使用不到的问题
        loc = locals()
        exec(f"result = {func}")
        return str(loc["result"])

    @classmethod
    def handle_env(cls, env: str) -> dict:
        env_list = env.split('&')
        env_dict = {}
        for e in env_list:
            env_dict[e.split("=")[0]]=e.split("=")[1]
        return env_dict

class HttpRequest(Session):
    """请求类实现"""

    def __init__(self):
        self._last_response = None
        super().__init__()

    @property
    def response(self) -> Response:
        return self._last_response

    @response.setter
    def response(self, value):
        self._last_response = value

    def send_request(
            self, method, url, header=None, file=None, send_content=None, **kwargs
    ):
        # send_content = {"send_param": {"page": 1, "size": 10},
        #              "send_data": {"userName": "19900009001", "passWord": "a123456"},
        #              "send_json": {"userName": "19900000000", "passWord": "a123456"}}

        extra_args = {}
        if "send_param" in send_content:
            extra_args["params"] = DataProcess.handle_data(str(send_content["send_param"]))
        if "send_data" in send_content:
            extra_args["data"] = DataProcess.handle_data(str(send_content["send_data"]))
        if "send_json" in send_content:
            extra_args["json"] = DataProcess.handle_data(str(send_content["send_json"]))

        urllib3.disable_warnings()
        self.response = self.request(
            method=method, url=url, files=file, headers=header, verify=False, **extra_args, **kwargs
        )
        logger.info(self.response.request.headers)
        logger.info(self.response.request.body)
        logger.info(self.response.request.url)
        req_info = {
            "请求地址": url,
            "请求方法": method,
            "请求头": header,
            "请求数据": extra_args,
            "上传文件": str(file),
        }
        ReportStyle.step("Request Info", req_info)
        logger.info(req_info)
        rep_info = {
            "响应耗时(ms)": self.response.elapsed.total_seconds() * 1000,
            "状态码": self.response.status_code,
            "响应数据": self.response.text,
            "响应数据头": str(self.response.headers),
        }
        logger.info(rep_info)
        ReportStyle.step("Response Info", rep_info)


class Validator(object):
    params = None
    value = None

    def istype_validator(self, params, value: str):
        mark = 0
        if isinstance(params, list):
            if value == "list":
                return True
            for each in params:
                if type(each).__name__ == "unicode":
                    if type(each.encode("utf-8")).__name__ in value:
                        mark += 1
                else:
                    if type(each).__name__ in value:
                        mark += 1
            if len(params) == mark:
                return True
            else:
                print("params list里有不满足的元素类型")
                return False
        elif type(params).__name__ == value:
            return True
        else:
            print(type(params).__name__)
            return False

    def intype_validator(self, params, value: list):
        mark = 0
        if isinstance(params, list):
            for each in params:
                if type(each).__name__ == "unicode":
                    if type(each.encode("utf-8")).__name__ in value:
                        mark += 1
                else:
                    if type(each).__name__ in value:
                        mark += 1
            if len(params) == mark:
                return True
            else:
                print("params list里有不满足的元素类型")
                return False
        elif type(params).__name__ in value:
            return True
        else:
            print(type(params).__name__)
            return False

    def isequal_validator(self, params, value):
        try:
            if "*" in str(value) or "/" in str(value) or "+" in str(value) or "-" in str(value):
                value = eval(value)
        except Exception as e:
            raise AssertionError("处理运算符时出错！")
        if params == value:
            return True
        else:
            return False

    def isnot_equal_validator(self, params, value):
        if params != value:
            return True
        else:
            return False

    # 检查params或paramslist中的每个元素的长度都等于value
    def len_validator(self, params, value):
        mark = 0
        if isinstance(params, list):
            for each in params:
                if len(each) == value:
                    mark += 1
            if len(params) == mark:
                return True
        else:
            if len(params) == value:
                return True
        return False

    # 检查paramslist的长度等于value
    def size_validator(self, params, value):
        if isinstance(params, list):
            if len(params) == value:
                return True
        return False

    # 检查params大于等于value
    def greater_validator(self, params, value):
        if isinstance(value, str):
            if params >= eval(value):
                return True
        else:
            if params >= value:
                return True
        return False

    # 检查params小于等于value
    def less_than_validator(self, params, value):
        if isinstance(value, str):
            if params <= eval(value):
                return True
        else:
            if params <= value:
                return True
        return False

    # 检测param在array[0]和array[1]之间
    def between_validator(self, params, value: array):
        mark = 0
        if isinstance(params, list):
            for each in params:
                if not isinstance(params, int):
                    return False
                if value[0] <= each <= value[1]:
                    mark += 1
            if mark == len(params):
                return True
        else:
            if not isinstance(params, int):
                return False
            if value[0] <= params <= value[1]:
                return True
        return False

    # 检测param存在于value（str或strlist）中
    def in_validator(self, param, value):
        if isinstance(value, list):
            if param in value:
                return True
            else:
                return False

    # 检测param(str)包含value(str)
    def contain_validator(self,param,value):
        if isinstance(value, str):
            if value in param:
                return True
        return False
        pass


class Report(object):

    @classmethod
    def create_allure_env_file(cls):
        file_path = os.path.join(base_dir, "report/html/environment.properties")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write("systemVersion="+platform.uname()[3])
            f.write("\n")
            f.write("pythonVersion="+platform.python_version())
            f.write("\n")
            f.write("author="+getpass.getuser())
            f.write("\n")
            f.write("email="+rfc.get_config('$.email.context.to').current[0])
    @classmethod
    def save_report_image(cls):
        def driver_(test=1):
            if test == 1:
                chrome_options = webdriver.ChromeOptions()
                chrome_options.add_argument('--headless')
                driver = webdriver.Chrome(chrome_options=chrome_options)
            else:
                # 启动浏览器
                driver = webdriver.Chrome()
            return driver

        driver = driver_(1)
        driver.get('http://localhost:63342/apiAutoTest-v3/report/html/index.html#')
        driver.get(
            'http://localhost:63342/apiAutoTest-v3/report/html/index.html#')
        sleep(1)
        file_path_overview = os.path.join(base_dir, 'report/html/report_overview.png')
        # driver.get_screenshot_as_file(file_path)  # 截图操作
        driver.save_screenshot(file_path_overview)  # 截图操作
        # 报告页面2
        driver.get(
            'http://localhost:63342/apiAutoTest-v3/report/html/index.html#graph')
        sleep(1)
        file_path_graph = os.path.join(base_dir, 'report/html/report_graph.png')
        driver.get_screenshot_as_file(file_path_graph)
        # driver.quit()


import base64
import hmac
import time
import json
import copy

class Jwt(object):

    @staticmethod  # 静态方法的装饰器封装一下  专门负责做计算用的函数
    def encode(self_payload, key, exp=300):
        # self_payload  含有私有声明的字典
        # key 自定的key
        # exp 过期时间

        # 生成header
        # header = {'typ': 'JWT', 'alg': 'HS256'}
        header = {"kid": "53bccad893e349ce93e497fc9e3f351", "alg": "ES256"}
        # header_json = json.dumps(header)  # 这样转为json串不行，有空格，损耗带宽
        header_json = json.dumps(header, separators=(',', ':'), sort_keys=True)
        # 这样逗号冒号前后就没有空格了,sort_keys=True 使出来的json串变的有序了，在做hmac或其他哈希的计算的时候，串值一定是稳定的
        # separators分割符 第一个参数代表的是每个键值对之间用什么分割，第二个参数是每个键和值之间用什么分割
        # sort_keys 生成有序的json串
        header_json_base64 = Jwt.b64encode(header_json.encode())

        # init payload
        self_payload_copy = copy.deepcopy(self_payload)  # 为了不污染传进来的字典
        # 给拷贝出来的字典中加入公有声明
        self_payload_copy["exp"] = time.time() + exp  # 过期时间
        self_payload_copy_json = json.dumps(self_payload_copy, separators=(',', ':'), sort_keys=True)
        self_payload_copy_json_base64 = Jwt.b64encode(self_payload_copy_json.encode())

        # init sign
        hm = hmac.new(key.encode(), header_json_base64 + b'.' + self_payload_copy_json_base64,
                      digestmod="SHA256")  # 两个都是字节码所以连接符*点*也要是字节码
        hm_base64 = Jwt.b64encode(hm.digest())  # 取hm的二进制结果，然后进行base64的转码

        # jwt token 诞生  字节码
        return header_json_base64 + b'.' + self_payload_copy_json_base64 + b'.' + hm_base64

    @staticmethod
    def b64encode(js):  # 为了将base64转换修改为urlsafe
        return base64.urlsafe_b64encode(js).replace(b"=", b"")

    @staticmethod
    def b64decode(bs):
        # 加回来等号
        rem = len(bs) % 4  # 取余
        if rem > 0:
            bs += b'=' * (4 - rem)

        return base64.urlsafe_b64decode(bs)

    @staticmethod
    def decode(token, key):
        # 传入jwt的值(令牌) 和只有调用者知道的key

        # 校验签名
        header_bs, payload_bs, signature_bs = token.split(b".")  # 因为是字节串
        hm = hmac.new(key.encode(), header_bs + b"." + payload_bs, digestmod="SHA256")
        if signature_bs != Jwt.b64encode(hm.digest()):  # 将签名结果和传过来的sign进行对比
            raise

        # 校验时间
        payload_js = Jwt.b64decode(payload_bs)  # 解码为json
        payload = json.loads(payload_js)  # 解码为字典

        now = time.time()  # 当前时间
        if int(now) > int(payload["exp"]):  # 登录时间过期
            raise
        return payload  # 返回自定义内容


header={
  "kid": "53bccad893e349ce93e497fc9e3f351",
  "alg": "ES256"
}
payload = {
  "iss": "https://www.talkdeskqaid.com",
  "aud": "https://api.talkdeskqa.com",
  "exp": 1650388326,
  "nbf": 1650387726,
  "iat": 1650387726,
  "jti": "8885b227fcbb4293abf5ef4cf49f25e8",
  "cid": "4af2f201e73e4766bd9dfa6431810790",
  "gty": "refresh_token",
  "scp": [
    "account-phones:read",
    "account:read",
    "acls:read",
    "assets:download",
    "assets:read",
    "assets:write",
    "callbar",
    "case-actions:read",
    "case-automation-groups:read",
    "case-automation-groups:write",
    "case-automations:read",
    "case-automations:write",
    "case-conditions:read",
    "case-dicts:read",
    "case-events-pool:read",
    "case-fields:read",
    "case-fields:write",
    "case-forms:read",
    "case-forms:write",
    "case-notes:read",
    "case-notes:write",
    "case-sla-policies:read",
    "case-sla-policies:write",
    "case-sla-targets:read",
    "case-sla-targets:write",
    "case-tags-manage:read",
    "case-tags:read",
    "case-tenant-apps:read",
    "case-tenant-permission:write",
    "case-views:read",
    "case-views:write",
    "cases:read",
    "cases:write",
    "contacts:read",
    "cti:messaging",
    "customer-data-store:read",
    "email-touchpoints:read",
    "graph-users:read",
    "groups:read",
    "interaction-contacts:read",
    "notifications:read",
    "omnichannel-inbox:write",
    "openid",
    "policies:evaluate",
    "proxy:read",
    "recordings:read",
    "tenant-config:read",
    "voicemails:read"
  ],
  "rlm": "MAIN",
  "aid": "609a416bfc6bb0e4b57459b6",
  "acc": "email-qa",
  "bbb": "11111",
  "psn": "INTERNAL",
  "sub": "1f3361bb7d234b02ad9eec9391e3161b",
  "usr": "jun.han@china.talkdesk.com",
  "uid": "624eac3ec8e99045f13c2c8e",
  "sid": "cd438f158526486699ebc3dc069c641e"
}
s = Jwt.encode(payload, "1234567", 300)
print(s.decode("utf-8"))
print(Jwt.decode(s, "1234567"))

# Report.create_allure_env_file()